Garbage Collector

Управляемая куча

Управляемая куча может быть представлена лентой битов. Такая куча есть у каждого процесса своя. Для 32 разрядных максимальный размер до 1.5гб, а для 64разрядных 8 тб.

В CLR оператор new:

  1. Подсчитывает количество байтов, необходимых для размещения полей типа. (С учетом системных блока синхронизации и указателя на объект типа)
  2. Проверяет хватает ли памяти и если нет, то запрашивает или сборку запускает
  3. Перемещает указатель на конец кучи и зануляет все байты внутри переданной памяти.
  4. Отдает указатель.

Алгоритм сборки мусора в рамках одного поколения

  • Способ 1
  1. Каждый объект имеет номер указывающий на число ссылок в коде на данный объект. Во время сборки убивается все у чего счетчик на 0.

Такой способ имеет недостаток в виде циклических зависимостей которые нужно выявлять.

  • Способ 2

Отслеживание ссылок

  1. Все переменные ссылочных типов называются корнями.
  2. Когда CLR запускает сборку мусора, она сначала приостанавливает все программные потоки.
  3. Запускается этап маркировки, CLR перебирает все объекты в куче и в поле блока синхронизации ставит 0. Затем CLR проходиться по всем корням и находит все объекты на которые ссылается корень. Если корень ссылается на объект, то в блоке синхронизации ставится 1 и начинаются проверяться все корни в этом объекте. Встретив уже помеченный объект сборщик останавливается чтобы не попасть в цикл.
  4. После маркировки все объекты без маркировки не переживают сборку мусора.
  5. Далее происходит сжатие, все объекты съезжают влево на освобожденные места от мертвых объектов.
  6. Далее ссылки внутри корней уменьшается на значение, на которое был смещен целевой объект.

Примечание, статическое поле хранит объект бессрочно, поэтому утечки памяти могут возникать, если такое поле хранит ссылку на коллекцию не нужных объектов.

Чтобы точно указать, когда пора убивать объект нужно вызывать .Dispose().

Такой метод может и продлить жизнь, так, если использовать переменную таймер, которая будет раз во сколько то раз что то делать, то есть вероятность что она умрет до завершения метода где была определена.

Поколения

Алгоритм поколений строиться на следующих предположениях:

  1. Чем младше объект, тем короче его время жизни.
  2. Чем старше объект, тем длиннее его время жизни.
  3. Сборка мусора в части кучи выполняется быстрее чем во всей.

Алгоритм:

  1. Объекты добавляются в поколение 0, до момента пока оно не будет заполнено, и объект некуда будет совать.
  2. Тогда запускается сборка мусора и сжатие, то что пережило одну сборку переходит в поколение 1.
  3. Когда заполняется околение 1, начинается сборка, выжившие становятся поколением 2.

CLR умеет сам анализировать код и выбирать оптимальные границы.

Если памяти не хватает, то перед OutOfMemoryException происходит полная сборка мусора.

Класс GCNotification выдает событие при собрки мусора в поколении 0 или 2.

Запуск сборки мусора

  • Вызов статического метода Collect класса System.GC
  • Windows сообщает о нехватке памяти.
  • Завершение работы CLR.
  • Выгрузка домена приложения.

Большие объекты

В .Net большими объектами считаются объекты больше 8500 байт.

Такие объекты хранятся в отдельном месте Large Object Heap

В отличии от обычной кучи, в ней не происходит сжатия, так как перекопировать очень трудозатратно. Эта отдельная зона памяти считается поколением 2 и чиститься только вместе со всем остальным поколением 2. По этой причине большие объекты следует создавать долгоживущими.

Режим сборки мусора

  • Режим рабочей станции, он нужен когда приложение работает на стороне клиента. Он старается минимально по времени тормозить все потоки приложения и минимально нагружать процессор, т.к. он работает не один в системе.
  • Режим сервера. В нем приложение подразумевает что работает на машине одно и во время сборки делит кучу на разделы и обрабатывает каждый из разделов на своем процессоре. Это достигается тем, что разделы выполняются в разных потоков, каждый а своем процессоре.

ASP.NET Core по умолчанию использует серверную сборку мусора.

Серверную сборку мусора можно включить через конфигурационный файл CLR.

Также существуют подрежимы сборщика мусора.

  • Параллельный (по умолчанию). В таком режиме есть фоновый поток.
    Алгоритм:
    1. Сборка начинается, потоки останавливаются, происходит подсчет, в каких поколениях нужно чистить память.
    2. Если чистка затронет поколение 2, то потоки размораживаются и фоновый поток производит параллельную маркировку. (А объект из-за которого уборка и началась, временно добавляется в поколение 0 сверх лимита)
    3. Потоки снова останавливаются, происходит чистка и сжатие.

Запретить параллельную сборку мусора также можно через конфиг CLR.

Принудительная сборка мусора.

И такой сборки есть два режима, форсед который собирает везде и оптимайзед который собирает только там где надо и где есть смысл.

Сборка происходит через System.GC

Почти во всех случаях лезть в сборщик не нужно. Но есть уникальные случаи когда все таки надо

  1. Если произошел уникальный случай и умерло сразу много объектов, в таком случае, прогнозы коллектора могут и ошибаться и лучше удалить принудительно.
  2. Если сборка мусора может занять много времени и клиентские запросы начнут протухать. В таком случае можно использовать специальные события указывающие на то что коллектор скоро запуститься и запускать принудительно, когда наиболее удобно.

Финализация

Метод финализации пишется как конструктор с припиской ~SomeClass()

Финализация нужна для освобождения ресурсов системы, это могут быть дескрипторы или потоки.

(В первых версиях шарпа финализаторы назывались дескрипторами, но потом отказались, т.к. не похоже и путает)

Метод финализации запускается, если он был переопределен. (Есть виртуальный метод в классе обжект) Также, в IL метод пишется как Finalize()

Метод финализации запускается перед сборкой этого объекта из кучи.

Алгоритм:

  1. Коллектор находит объект.
  2. Коллектор видит переопределенную финализацию и ставит этот метод на выполнение отдельным потоком занимающимся финализацией.
  3. Коллектор переводит все объекты из полей, а также сам объект в следующее поколение.
  4. Поток финализации запускает финализатор.

Нюансы:

  • Если блокнуть поток в финализации, то будет утечка памяти для всех объектов с финализацией в будущем.
  • Перевод в другое поколение - оч плохо, поэтому финализацию лучше не применять.
  • В финализации можно использовать другие ссылочне переменные, с этим проблем нет.
  • Запуск финализаторов выполняется однопоточно

IDispose

Из-за описаных проблем рекомендуется использовать интерфейс IDispose;

Паттерн заключается в реализации интерфейса IDispose, который имеет один метод Dispose.

Также требуется использовать конструкции using, которые разворачиваются в try finally, который запускает реализованный Dispose.

Также, можно реализовывать и IDispose и финализатор, на случай если по какой то причине finally не сработал.

Using можно вкладывать друг в друга.